using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Red.Core.Extensions;
using P4 = Perforce.P4;

namespace Red.Core.Perforce
{
    /// <summary>
    /// Wraps Perforce.P4.LineEnd
    /// </summary>
    /// <remarks>
    /// Order must match p4 api
    /// </remarks>
    public enum LineEndings
    {
        [Description( "Local Operating System" )]
        Local,

        [Description( "Unix \\n" )]
        Unix,

        [Description( "Mac \\r" )]
        Mac,

        [Description( "Windows \\r\\n" )]
        Windows,

        [Description( "Read Local Write Unix" )]
        Shared
    }

    /// <summary>
    /// Wraps Perforce.P4.SubmitType
    /// </summary>
    /// <remarks>
    /// Order must match p4 api
    /// </remarks>
    public enum SubmitType
    {
        [Description( "Submit All" )]
        Submit_all,

        [Description( "Revert unchanged files before submit" )]
        Revert_unchanged,

        [Description( "Submit changed files only (leave unchanged checked out)" )]
        Submit_changed_only,
    }

    public class FileStatus
    {
        public bool CheckedOutByMe { get; internal set; }
        public bool CheckedOutByOthers { get; internal set; }
        public bool IsLatestRevision { get; internal set; }
        public bool IsMarkedForAdd { get; internal set; }
        public bool NotUnderSourceControl { get; internal set; }
        public IList<string> CheckedOutBy { get; internal set; }
    }

    public class Mediator
    {
        #region Data

        private P4.Repository _repository;
        private string _p4Server;
        private string _p4User;
        private string _p4Workspace;
        private bool _initialized;

        #endregion Data

        #region Properties

        public bool IsInitialized => _initialized;
        public string Username => _p4User;

        #endregion Properties

        #region Events

        public delegate void ErrorEventHandler( string message );
        public delegate void NotSignedInEventHandler( string username );

        public delegate void ChangelistEventHandler( int changelistId );
        public delegate void OperationStartedEventHandler( IEnumerable<string> filePaths );

        /// <summary>
        /// Function signature of event called when a perforce operation is completed
        /// </summary>
        /// <param name="requested">The complete list of files sent to perforce at the start of the operation</param>
        /// <param name="modified">The subset of <see cref="requested"/> that were actually changed by the perforce operation</param>
        public delegate void OperationFinishedEventHandler( IEnumerable<string> requested, IEnumerable<string> modified );
        private delegate T InternalOperation<T>();
        private delegate T InternalChangelist<T>( out int changelistId );

        public event ErrorEventHandler Error;
        public event NotSignedInEventHandler NotSignedIn;

        public event ChangelistEventHandler ChangelistAdded;
        public event ChangelistEventHandler ChangelistDeleted;
        public event ChangelistEventHandler ChangelistSubmitted;
        public event ChangelistEventHandler ChangelistUpdated;

        public event OperationStartedEventHandler CheckoutStarted;
        public event OperationFinishedEventHandler CheckoutFinished;

        public event OperationStartedEventHandler GetLatestStarted;
        public event OperationFinishedEventHandler GetLatestFinished;

        public event OperationStartedEventHandler RevertStarted;
        public event OperationFinishedEventHandler RevertFinished;

        public event OperationStartedEventHandler AddStarted;
        public event OperationFinishedEventHandler AddFinished;

        public event OperationStartedEventHandler DeleteStarted;
        public event OperationFinishedEventHandler DeleteFinished;

        public event OperationStartedEventHandler GetChangelistsStarted;
        public event OperationFinishedEventHandler GetChangelistsFinished;

        #endregion Events

        #region Private methods

        private bool CheckConnection( string password = null )
        {
            if ( _repository?.Connection.Status == P4.ConnectionStatus.Connected && _repository?.Connection.Credential != null )
                return true;

            if ( !_initialized )
                return false;
            
            if( _repository == null )
            {
                P4.Server server = new P4.Server( new P4.ServerAddress( _p4Server ) );
                _repository = new P4.Repository( server );

                _repository.Connection.UserName = _p4User;
                _repository.Connection.Client = new P4.Client { Name = _p4Workspace };
            }

            P4.Connection connection = _repository.Connection;

            if ( password != null )
            {
                try
                {
                    connection.Login( password );
                }
                catch ( P4.P4Exception ex )
                {
                    // This code is taken from Perforce ("unknown")
                    const int AuthFailedErrorCode = 0x30021eb1;

                    // If the user entered the wrong password, return false, if there was another reason for the failure, let the code below figure out what's up
                    if ( ex.ErrorCode != AuthFailedErrorCode )
                    {
                        return false;
                    }
                }
            }

            try
            {
                connection.Connect( null );

                if ( !connection.LastResults.Success )
                {
                    bool isUserSignedIn = connection.LastResults.ErrorList.Find( error => error.ErrorCode == P4.P4ClientError.MsgServer_BadPassword ) == null;

                    if( !isUserSignedIn )
                    {
                        NotSignedIn?.Invoke( _p4User );
                        return false;
                    }

                    string errorMessage = string.Join( "\n", from error in connection.LastResults.ErrorList select error.ErrorMessage );

                    _repository = null;
                    Error?.Invoke( errorMessage );
                    return false;
                }
            }
            catch ( P4.P4Exception ex )
            {
                _repository = null;
                Error?.Invoke( ex.Message );
                return false;
            }

            return true;
        }

        // Paths passed to this method can be local paths or depot paths
        private IEnumerable<P4.FileSpec> PrepareFileSpecs( IEnumerable<string> paths, P4.VersionSpec versionSpec = null )
        {
            return from path in paths select new P4.FileSpec( new P4.DepotPath( path ), versionSpec );
        }

        private T ChangelistInternalWrapper<T>( ChangelistEventHandler clEvent, InternalChangelist<T> func )
        {
            try
            {
                if ( !CheckConnection() )
                    return default( T );

                int changelistId;
                T retVal = func( out changelistId );
                clEvent?.Invoke( changelistId );

                return retVal;
            }
            catch ( P4.P4Exception exception )
            {
                Error?.Invoke( exception.Message );
            }
            catch ( NullReferenceException )
            {
                Error?.Invoke( "P4.Changelist does not exist!" );
            }

            return default( T );
        }

        private IEnumerable<T> InternalWrapper<T>( OperationStartedEventHandler startedEvent, OperationFinishedEventHandler finishedEvent, IEnumerable<string> files, InternalOperation<IEnumerable<T>> internalOperation )
        {
            startedEvent?.Invoke( files );

            try
            {
                if ( !CheckConnection() )
                    return Enumerable.Empty<T>();

                IEnumerable<T> modified = internalOperation();

                finishedEvent?.Invoke( files, from file in modified select ( file as P4.FileMetaData )?.LocalPath.Path ?? ( file as P4.FileSpec )?.LocalPath.Path );

                return modified;
            }
            catch ( P4.P4Exception exception )
            {
                Error?.Invoke( exception.Message );
                return Enumerable.Empty<T>();
            }
        }

        private T InternalWrapper<T>( InternalOperation<T> internalOperation )
        {
            try
            {
                if ( !CheckConnection() )
                    return default( T );

                return internalOperation();
            }
            catch ( P4.P4Exception exception )
            {
                Error?.Invoke( exception.Message );
                return default( T );
            }
        }

        private IEnumerable<P4.FileMetaData> GetFileMetaDataInternal( IEnumerable<string> depotPaths, P4.VersionSpec versionSpec = null )
        {
            var fileSpecs = PrepareFileSpecs( depotPaths, versionSpec ).ToList();
            var fileMetaDataCmdOptions = new P4.GetFileMetaDataCmdOptions( P4.GetFileMetadataCmdFlags.None, null, null, fileSpecs.Count, null, null, null );
            var obtainedFileMetaData = _repository.GetFileMetaData( fileSpecs, fileMetaDataCmdOptions );
            return obtainedFileMetaData ?? Enumerable.Empty<P4.FileMetaData>();
        }

        private IEnumerable<P4.FileMetaData> GetFileMetaDataInternal( IEnumerable<string> depotPaths, int changelistId )
        {
            var fileSpecs = PrepareFileSpecs( depotPaths ).ToList();
            /* 
             * GetFileMetaData throws an exception if we pass "-1" (default changelist id)
             * In order to get files for default changelist, we need to pass null as changelist id.
             */
            var changelist = changelistId == -1 ? null : changelistId.ToString();
            var fileMetaDataCmdOptions = new P4.GetFileMetaDataCmdOptions( P4.GetFileMetadataCmdFlags.Opened, null, null, fileSpecs.Count, null, changelist, null );
            var obtainedFileMetaData = _repository.GetFileMetaData( fileSpecs, fileMetaDataCmdOptions );
            return obtainedFileMetaData ?? Enumerable.Empty<P4.FileMetaData>();
        }

        private IEnumerable<P4.FileSpec> CheckoutFilesInternal( IEnumerable<string> localPaths, int changelistId = -1 )
        {
            var fileSpecs = PrepareFileSpecs( localPaths ).ToList();
            var editCmdOptions = new P4.EditCmdOptions( P4.EditFilesCmdFlags.None, changelistId, null );
            var editedFiles = _repository.Connection.Client.EditFiles( fileSpecs, editCmdOptions );
            return editedFiles ?? Enumerable.Empty<P4.FileSpec>();
        }

        private IEnumerable<P4.FileSpec> RevertFilesInternal( IEnumerable<string> localPaths, bool unchangedOnly )
        {
            var fileSpecs = PrepareFileSpecs( localPaths ).ToList();
            var flags = (unchangedOnly)? P4.RevertFilesCmdFlags.UnchangedOnly : P4.RevertFilesCmdFlags.None;
            var revertCmdOptions = new P4.RevertCmdOptions( flags, -1 );
            var revertedFiles = _repository.Connection.Client.RevertFiles( fileSpecs, revertCmdOptions );
            return revertedFiles ?? Enumerable.Empty<P4.FileSpec>();
        }

        private IEnumerable<P4.FileSpec> GetLatestInternal( IEnumerable<string> localPaths, bool force )
        {
            var fileSpecs = PrepareFileSpecs( localPaths ).ToList();
            var syncCmdOptions = new P4.SyncFilesCmdOptions( force? P4.SyncFilesCmdFlags.Force : P4.SyncFilesCmdFlags.None, 0 );
            var syncedFiles = _repository.Connection.Client.SyncFiles( fileSpecs, syncCmdOptions );
            return syncedFiles ?? Enumerable.Empty<P4.FileSpec>();
        }

        private IEnumerable<P4.FileSpec> AddFilesInternal( IEnumerable<string> localPaths, int changelistId )
        {
            var fileSpecs = PrepareFileSpecs( localPaths ).ToList();
            var addCmdOptions = new P4.AddFilesCmdOptions( P4.AddFilesCmdFlags.None, changelistId, null );
            var addedFiles = _repository.Connection.Client.AddFiles( fileSpecs, addCmdOptions );
            return addedFiles ?? Enumerable.Empty<P4.FileSpec>();
        }

        private IEnumerable<P4.FileSpec> DeleteFilesInternal( IEnumerable<string> localPaths, int changelistId )
        {
            var fileSpecs = PrepareFileSpecs( localPaths ).ToList();
            var addCmdOptions = new P4.DeleteFilesCmdOptions( P4.DeleteFilesCmdFlags.None, changelistId );
            var deletedFiles = _repository.Connection.Client.DeleteFiles( fileSpecs, addCmdOptions );
            return deletedFiles ?? Enumerable.Empty<P4.FileSpec>();
        }

        public IEnumerable<P4.Changelist> GetChangelistsInternal( IEnumerable<string> localPaths )
        {
            var fileSpecs = PrepareFileSpecs( localPaths ).ToList();
            P4.FileSpec[] files = fileSpecs.Any()? fileSpecs.ToArray() : null;

            var changesCmdOptions = new P4.ChangesCmdOptions( P4.ChangesCmdFlags.FullDescription, _p4Workspace, 0, P4.ChangeListStatus.Pending, _p4User );
            var changelists = _repository.GetChangelists( changesCmdOptions, files );
            return changelists ?? Enumerable.Empty<P4.Changelist>();
        }

        private P4.Changelist CreateChangelistInternal( string description )
        {
            var newChangelist = new P4.Changelist { Description = description };
            newChangelist = _repository.CreateChangelist( newChangelist );
            return newChangelist;
        }

        private bool DeleteChangelistInternal( int changelistId )
        {
            var changelistToDelete = _repository.GetChangelist( changelistId );
            return DeleteChangelistInternal( changelistToDelete );
        }

        private bool DeleteChangelistInternal( P4.Changelist changelist )
        {
            _repository.DeleteChangelist( changelist, null );

            return true;
        }

        public int UpdateChangelistInternal( int changelistId, string description, IEnumerable<string> filesToRemoveFromChangelist = null )
        {
            var changelist = _repository.GetChangelist( changelistId );
            changelist.Description = description;

            if ( filesToRemoveFromChangelist != null )
            {
                ( changelist.Files as List<P4.FileMetaData> ).RemoveAll( item => filesToRemoveFromChangelist.Contains( item.DepotPath.Path ) );
            }

            _repository.UpdateChangelist( changelist );

            return changelist.Id;
        }

        public string GetChangelistDescriptionInternal( int changelistId )
        {
            var changelist = _repository.GetChangelist( changelistId );
            if ( changelist.Description.Equals( "<enter description here>" ) )
                return string.Empty;

            return changelist.Description;
        }
        
        public int SubmitFilesInternal( string description, IEnumerable<string> paths )
        {
            var newChangelist = CreateChangelistInternal( description );

            var specs = PrepareFileSpecs( paths );
            _repository.Connection.Client.ReopenFiles( specs.ToList(), new P4.ReopenCmdOptions( newChangelist.Id, null ) );

            SubmitChangelistInternal( newChangelist );

            return newChangelist.Id;
        }

        private int SubmitChangelistInternal( int changelistId )
        {
            var changelist = _repository.GetChangelist( changelistId );
            SubmitChangelistInternal( changelist );

            return changelist.Id;
        }

        private void SubmitChangelistInternal( P4.Changelist changelist )
        {
            var clientOptions = new P4.ClientSubmitOptions( false, P4.SubmitType.SubmitUnchanged );
            var submitOptions = new P4.SubmitCmdOptions( P4.SubmitFilesCmdFlags.None, changelist.Id, null, null, clientOptions );
            changelist.Submit( submitOptions );
        }

        private P4.Client CreateWorkspaceCommon( string name, string stream, SubmitType submitType, LineEndings lineEnd, bool allWrite, bool clobber, bool compress, bool modTime, bool rmdir )
        {
            P4.ClientCmdOptions cmdOptions = new P4.ClientCmdOptions( P4.ClientCmdFlags.None );
            P4.Client workspace = _repository.GetClient( name, cmdOptions );

            workspace.Stream = stream;

            workspace.Options |= ( allWrite ) ? P4.ClientOption.AllWrite : P4.ClientOption.None;
            workspace.Options |= ( clobber ) ? P4.ClientOption.Clobber : P4.ClientOption.None;
            workspace.Options |= ( compress ) ? P4.ClientOption.Compress : P4.ClientOption.None;
            workspace.Options |= ( modTime ) ? P4.ClientOption.ModTime : P4.ClientOption.None;
            workspace.Options |= ( rmdir ) ? P4.ClientOption.RmDir : P4.ClientOption.None;

            workspace.SubmitOptions.SubmitType = (P4.SubmitType)(int)submitType;
            workspace.LineEnd = (P4.LineEnd)(int)lineEnd;

            return workspace;
        }

        private P4.Client CreateWorkspaceInternal( string name, string root, string stream, SubmitType submitType, LineEndings lineEnd, bool allWrite, bool clobber, bool compress, bool modTime, bool rmdir )
        {
            P4.Client workspace = CreateWorkspaceCommon( name, stream, submitType, lineEnd, allWrite, clobber, compress, modTime, rmdir );
            workspace.Root = root;

            return _repository.CreateClient( workspace );
        }

        private P4.Client UpdateWorkspaceInternal( string name, string stream, SubmitType submitType, LineEndings lineEnd, bool allWrite, bool clobber, bool compress, bool modTime, bool rmdir )
        {
            P4.Client workspace = CreateWorkspaceCommon( name, stream, submitType, lineEnd, allWrite, clobber, compress, modTime, rmdir );
            return _repository.UpdateClient( workspace );
        }

        private P4.Client ChangeWorkspaceRootInternal( string name, string root )
        {
            P4.ClientCmdOptions cmdOptions = new P4.ClientCmdOptions( P4.ClientCmdFlags.None );
            P4.Client workspace = _repository.GetClient( name, cmdOptions );

            workspace.Root = root;
            return _repository.UpdateClient( workspace );
        }

        private bool DeleteWorkspaceInternal( string name )
        {
            P4.ClientCmdOptions cmdOptions = new P4.ClientCmdOptions( P4.ClientCmdFlags.None );
            P4.Client workspace = _repository.GetClient( name, cmdOptions );

            _repository.DeleteClient( workspace, null );

            return true;
        }

        private string GetWorkspaceStreamInternal( string workspace )
        {
            P4.ClientCmdOptions options = new P4.ClientCmdOptions( P4.ClientCmdFlags.None );
            P4.Client client = _repository.GetClient( workspace, options );

            return client?.Stream;
        }

        private string CreateStreamInternal( StreamBuilder builder )
        {
            builder.Owner = _p4User;
            P4.Stream spec = builder.Construct();

            if( spec != null )
            {
                P4.Stream stream = _repository.CreateStream( spec );

                return stream.Id;
            }

            return null;
        }

        private bool DeleteStreamInternal( string id )
        {
            P4.Stream stream = _repository.GetStream( id );

            if( stream != null )
            {
                _repository.DeleteStream( stream, null );

                return true;
            }

            return false;
        }

        private IEnumerable<Stream> EnumerateStreamsInternal()
        {
            var streams = _repository.GetStreams();
            return from stream in streams select new Stream( stream.Name, stream.Id );
        }
        private IEnumerable<P4.FileMetaData> GetChangelistFilesInternal( int changelistId )
        {
            var changelist = _repository.GetChangelist( changelistId );
            return changelist.Files;
        }

        private bool DoesWorkspaceExist( P4.Repository repository, string userName, string workspace )
        {
            IList<P4.Client> clients;
            try
            {
                var clientsCmdOptions = new P4.ClientsCmdOptions( P4.ClientsCmdFlags.None, userName, workspace, 0, "" );
                clients = repository.GetClients( clientsCmdOptions );
            }
            catch ( P4.P4Exception )
            {
                // user doesn't exist
                return false;
            }

            return clients != null;
        }

        private ulong GetSizeInternal( string path )
        {
            ulong size = ( string.IsNullOrEmpty( path ) ) ? 0 : _repository.GetStreamSize( path );

            return size;
        }

        #endregion Private methods

        #region C-Tor

        public Mediator()
        {
        }

        public Mediator( string server, string user, string workspace )
        {
            _p4Server = server;
            _p4User = user;
            _p4Workspace = workspace;

            _initialized = true;
        }

        #endregion C-Tor

        #region Public methods

        public bool Initialize( Context p4Context )
        {
            if ( _initialized )
            {
                if ( _repository != null )
                {
                    _repository.Connection.Disconnect();
                    _repository = null;
                }
            }

            _p4Server = p4Context.Server;
            _p4User = p4Context.User;
            _p4Workspace = p4Context.Workspace;
            _initialized = true;

            return CheckConnection();
        }

        public bool SignIn( string password )
        {
            return CheckConnection( password );
        }

        public string CreateStream( StreamBuilder builder )
        {
            return InternalWrapper( () => { return CreateStreamInternal( builder ); } );
        }

        public bool DeleteStream( string stream )
        {
            return InternalWrapper( () => { return DeleteStreamInternal( stream ); } );
        }

        public bool CreateWorkspace( string name, string root, string stream, SubmitType submitType, LineEndings lineEnd, bool allWrite, bool clobber, bool compress, bool modTime, bool rmdir )
        {
            return InternalWrapper( () => { return CreateWorkspaceInternal( name, root, stream, submitType, lineEnd, allWrite, clobber, compress, modTime, rmdir ) != null; } );
        }

        public bool UpdateWorkspace( string name, string stream, SubmitType submitType, LineEndings lineEnd, bool allWrite, bool clobber, bool compress, bool modTime, bool rmdir )
        {
            return InternalWrapper( () => { return UpdateWorkspaceInternal( name, stream, submitType, lineEnd, allWrite, clobber, compress, modTime, rmdir ) != null; } );
        }

        public bool ChangeWorkspaceRoot( string name, string root )
        {
            return InternalWrapper( () => { return ChangeWorkspaceRootInternal( name, root ) != null; } );
        }

        public bool DeleteWorkspace( string name )
        {
            return InternalWrapper( () => { return DeleteWorkspaceInternal( name ); } );
        }

        /// <summary>
        /// Returns a list of all streams in the repository
        /// </summary>
        /// <returns>Each stream contains name and id</returns>
        public IEnumerable<Stream> GetStreams()
        {
            return InternalWrapper( () => EnumerateStreamsInternal() );
        }

        public string GetDepotRoot()
        {
            return InternalWrapper( () => _repository.Connection.Client.Root );
        }

        /// <summary>
        /// Wrapper for <see cref="GetLatest"/> that removes the return type
        /// </summary>
        /// <remarks>
        /// If you're not interested in what files are affected by the update, or you don't want to add a dependency on the P4 library, use this function
        /// </remarks>
        public void GetLatestNoReturn( IEnumerable<string> localPaths, bool force = false )
        {
            GetLatest( localPaths, force );
        }

        public bool CheckoutFile( string localPath, int changelistId )
        {
            return CheckoutFiles( new[] { localPath }, changelistId ).Count() != 0;
        }

        public IEnumerable<P4.FileMetaData> GetFileMetaData( IEnumerable<string> localPaths )
        {
            return InternalWrapper( null, null, localPaths, () => GetFileMetaDataInternal( localPaths ) );
        }

        public IEnumerable<P4.FileMetaData> GetFileMetaData( IEnumerable<string> localPaths, int changelistId )
        {
            return InternalWrapper( null, null, localPaths, () => GetFileMetaDataInternal( localPaths, changelistId ) );
        }

        public IEnumerable<P4.FileSpec> CheckoutFiles( IEnumerable<string> localPaths, int changelistId )
        {
            return InternalWrapper( CheckoutStarted, CheckoutFinished, localPaths, () => CheckoutFilesInternal( localPaths, changelistId ) );
        }

        public IEnumerable<P4.FileSpec> RevertFiles( IEnumerable<string> localPaths, bool unchangedOnly = false )
        {
            return InternalWrapper( RevertStarted,RevertFinished, localPaths, () => RevertFilesInternal( localPaths, unchangedOnly ) );
        }

        public IEnumerable<P4.FileSpec> GetLatest( IEnumerable<string> localPaths, bool force = false )
        {
            return InternalWrapper( GetLatestStarted, GetLatestFinished, localPaths, () => GetLatestInternal( localPaths, force ) );
        }

        public IEnumerable<P4.FileSpec> AddFiles( IEnumerable<string> localPaths, int changelistId )
        {
            return InternalWrapper( AddStarted, AddFinished, localPaths, () => AddFilesInternal( localPaths, changelistId ) );
        }

        public IEnumerable<P4.FileSpec> DeleteFiles( IEnumerable<string> localPaths, int changelistId )
        {
            return InternalWrapper( DeleteStarted, DeleteFinished, localPaths, () => DeleteFilesInternal( localPaths, changelistId ) );
        }

        public IEnumerable<P4.Changelist> GetChangelists()
        {
            return GetChangelists( new List<string>() );
        }

        public IEnumerable<P4.Changelist> GetChangelists( IEnumerable<string> localPaths )
        {
            return InternalWrapper( GetChangelistsStarted, GetChangelistsFinished, localPaths, () => GetChangelistsInternal( localPaths ) );
        }

        public int CreateChangelist( string description )
        {
            return ChangelistInternalWrapper( ChangelistAdded, ( out int changelistId ) => { changelistId = CreateChangelistInternal( description ).Id; return changelistId; } );
        }

        public bool DeleteChangelist( int changelistId )
        {
            return ChangelistInternalWrapper( ChangelistDeleted, ( out int changelistIdOut ) => { changelistIdOut = changelistId; return DeleteChangelistInternal( changelistId ); } );
        }

        public bool DeleteChangelist( P4.Changelist changelist )
        {
            return ChangelistInternalWrapper( ChangelistDeleted, ( out int changelistIdOut ) => { changelistIdOut = changelist.Id; return DeleteChangelistInternal( changelist ); } );
        }

        public string GetChangelistDescription( int changelistId = -1 )
        {
            return ChangelistInternalWrapper( null, ( out int changelistIdOut ) => { changelistIdOut = changelistId; return GetChangelistDescriptionInternal( changelistId ); } );
        }

        public bool IsChangelistEmpty( int changelistId = -1 )
        {
            return ChangelistInternalWrapper( null, ( out int changelistIdOut ) => { changelistIdOut = changelistId; return _repository.GetChangelist( changelistId ).Files.Count == 0; } );
        }

        public int SubmitChangelist( int changelistId )
        {
            return ChangelistInternalWrapper( ChangelistSubmitted, ( out int changelistIdOut ) => { changelistIdOut = SubmitChangelistInternal( changelistId ); return changelistIdOut; } );
        }

        public int SubmitFiles( string description, IEnumerable<string> paths )
        {
            return ChangelistInternalWrapper( ChangelistSubmitted, ( out int changelistIdOut ) => { changelistIdOut = SubmitFilesInternal( description, paths ); return changelistIdOut; } );
        }

        public int UpdateChangelist( int changelistId, string description, IEnumerable<string> filesToRemoveFromChangelist = null )
        {
            return ChangelistInternalWrapper( ChangelistUpdated, ( out int changelistIdOut ) => { changelistIdOut = UpdateChangelistInternal( changelistId, description, filesToRemoveFromChangelist ); return changelistIdOut; } );
        }

        public bool AnyFilesInDefaultChangelist()
        {
            return InternalWrapper( () => { P4.Changelist changelist = _repository.GetChangelist( -1 ); return changelist.Files.Any(); } );
        }

        public IEnumerable<P4.FileMetaData> GetChangelistFiles( int changelistId = -1 )
        {
            return ChangelistInternalWrapper( null, ( out int changelistIdOut ) => { changelistIdOut = changelistId; return GetChangelistFilesInternal( changelistId ); } );
        }

        public ulong GetSize( string stream )
        {
            return InternalWrapper( () => { return GetSizeInternal( stream ); } );
        }

        public string GetWorkspaceStream( string workspace )
        {
            return InternalWrapper( () => { return GetWorkspaceStreamInternal( workspace ); } );
        }

        public ulong GetWorkspaceSize()
        {
            return InternalWrapper( () => { return GetSizeInternal( _repository.Connection.Client.Stream ); } );
        }

        public bool HasValidStream()
        {
            return InternalWrapper( () => _repository.Connection.Client.Stream != null );
        }

        public FileStatus GetFileStatus( P4.FileMetaData metadata )
        {
            bool localFile = metadata == null;
            bool fileCheckedOut = !localFile && IsCheckedOutByMe( metadata );
            bool fileMarkedForAdd = !localFile && IsMarkedForAdd( metadata );
            bool checkedOutByOthers = metadata != null && metadata.OtherOpen != 0;

            FileStatus fileStatus = new FileStatus()
            {
                CheckedOutByMe = fileCheckedOut,
                CheckedOutByOthers = checkedOutByOthers,
                NotUnderSourceControl = localFile,
                IsLatestRevision = metadata?.HaveRev == metadata?.HeadRev,
                IsMarkedForAdd = fileMarkedForAdd,
                CheckedOutBy = metadata?.OtherLockUserClients ?? new List<string>()
            };

            return fileStatus;
        }

        public FileStatus GetFileStatus( string localPath )
        {
            try
            {
                if ( !CheckConnection() )
                    return null;

                var fileMetadata = GetFileMetaDataInternal( new List<string>() { localPath } ).FirstOrDefault();

                return GetFileStatus( fileMetadata );
            }
            catch ( P4.P4Exception exception )
            {
                Error?.Invoke( exception.Message );
                return null;
            }
        }

        public bool? IsFileEditable( string localPath )
        {
            try
            {
                if ( !CheckConnection() )
                    return null;

                var fileMetadata = GetFileMetaDataInternal( new List<string>() { localPath } ).FirstOrDefault();

                bool localFile = fileMetadata == null;
                bool fileCheckedOut = !localFile && IsCheckedOutByMe( fileMetadata );
                bool fileMarkedForAdd = !localFile && IsMarkedForAdd( fileMetadata );

                return localFile || fileCheckedOut || fileMarkedForAdd;
            }
            catch ( P4.P4Exception exception )
            {
                Error?.Invoke( exception.Message );
                return null;
            }
        }

        public bool IsCheckedOut( string localPath )
        {
            try
            {
                if ( !CheckConnection() )
                    return false;

                var metaData = GetFileMetaDataInternal( new List<string>() { localPath } ).FirstOrDefault();
                return metaData != null && metaData.OtherOpen != 0;
            }
            catch ( P4.P4Exception exception )
            {
                Error?.Invoke( exception.Message );
                return false;
            }
        }

        public bool IsCheckedOutByMe( P4.FileMetaData metaData )
        {
            return metaData.Action == P4.FileAction.Edit && metaData.ActionOwner == _p4User;
        }

        public bool IsMarkedForAdd( P4.FileMetaData metaData )
        {
            return metaData.Action == P4.FileAction.Add && metaData.ActionOwner == _p4User;
        }

        public bool IsDeleted( P4.FileMetaData metaData )
        {
            return metaData.Action == P4.FileAction.Delete && metaData.ActionOwner == _p4User;
        }

        public bool AreSettingsValid( string adress, string user, string workspace )
        {
            var server = new P4.Server( new P4.ServerAddress( adress ) );
            var repository = new P4.Repository( server );
            var connection = repository.Connection;

            connection.UserName = user;
            connection.Client = new P4.Client { Name = workspace };

            bool settingsValid;
            try
            {
                connection.Connect( null );
                settingsValid = DoesWorkspaceExist( repository, user, workspace );
            }
            catch ( P4.P4Exception )
            {
                connection.Disconnect();
                return false;
            }

            connection.Disconnect();
            return settingsValid;
        }

        #endregion Public methods
    }
}
